package com.android.volley.toolbox;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import android.os.SystemClock;
import android.text.TextUtils;

import com.android.volley.Cache;
import com.android.volley.VolleyLog;

/**
 * @description 图片磁盘缓存
 * @author <a href="mailto:zhongyang.zhao@downjoy.com">Barry.Zhao</a>
 * @time 2014-8-12 上午11:27:26
 * @version 7.0
 */
public class DiskImageBaseCache extends Cache {

    private static final String DEFAULT_CHARSET = "UTF-8";
    /** Map of the Key, CacheHeader pairs */
    protected final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>( 16, .75f, true);

    /** Total amount of space currently used by the cache in bytes. */
    protected long mTotalSize = 0;

    /** The root directory to use for the cache. */
    protected final File mRootDirectory;

    /** The maximum size of the cache in bytes. */
    protected final int mMaxCacheSizeInBytes;

    /** Default maximum disk usage in bytes. */
    protected static final int DEFAULT_DISK_USAGE_BYTES = 8 * 1024 * 1024;

    /** High water mark percentage for the cache */
    protected static final float HYSTERESIS_FACTOR = 0.9f;

    /** Magic number for current version of cache file format. */
    protected static final int CACHE_MAGIC = 0x20120504;

    private static final String IMAGE_DIR = ".img";
    /** the image directory to use for the cache **/
    private final File mImageDir;

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory.
     * @param rootDirectory The root directory of the cache.
     * @param maxCacheSizeInBytes The maximum size of the cache in bytes.
     */
    public DiskImageBaseCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mImageDir = new File( rootDirectory, IMAGE_DIR);
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory using the default maximum cache size of 5MB.
     * @param rootDirectory The root directory of the cache.
     */
    public DiskImageBaseCache(File rootDirectory) {
        this( rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

    /**
     * Clears the cache. Deletes all cached files from disk.
     */
    @Override
    public synchronized void clear() {
        File[] files = mRootDirectory.listFiles();
        if(files != null) {
            for(File file: files) {
                deleteFile( file);
                deleteFile( getImageFileForKey( file));
            }
        }
        mEntries.clear();
        mTotalSize = 0;
        VolleyLog.d( "Cache cleared.");
    }

    /**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get( key);
        // if the entry does not exist, return.
        if(entry == null) {
            return null;
        }

        File file = getFileForKey( key);
        FileInputStream in = null;
        try {
            in = new FileInputStream( file);
            CacheHeader.readHeader( in); // eat header
            return entry.toCacheEntry();
        } catch(IOException e) {
            VolleyLog.d( "%s: %s", file.getAbsolutePath(), e.toString());
            remove( key);
            return null;
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch(IOException ioe) {
                    return null;
                }
            }
        }
    }

    /**
     * Initializes the DiskBasedCache by scanning for all files currently in the specified root directory. Creates the root
     * directory if necessary.
     */
    @Override
    public synchronized void initialize() {
        if(!mRootDirectory.exists() || !mImageDir.exists()) {
            if(!mRootDirectory.exists()) {
                if(!mRootDirectory.mkdirs()) {
                    VolleyLog.e( "Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
                }
            }
            if(!mImageDir.exists()) {
                if(!mImageDir.mkdirs()) {
                    VolleyLog.e( "Unable to create cache dir %s", mImageDir.getAbsolutePath());
                }
            }
            return;
        }
        File[] files = mRootDirectory.listFiles();
        if(files == null) {
            return;
        }
        File[] imageFiles = mImageDir.listFiles();
        // 检查是否有未清除掉的数据在图片缓存目录
        List<File> imageFileList = new ArrayList<File>();
        if(imageFiles != null && imageFiles.length > 0) {
            for(File file:imageFiles){
                imageFileList.add( file);
            }
        }
        for(File file: files) {
            if(file.isDirectory())
                continue; 
            FileInputStream fis = null;
            try {
                fis = new FileInputStream( file);
                CacheHeader entry = CacheHeader.readHeader( fis);
                File imageFile = getImageFileForKey( file);
                entry.size = file.length() + imageFile.length();
                putEntry( entry.key, entry);
                imageFileList.remove( imageFile);
            } catch(IOException e) {
                if(file != null) {
                    file.delete();
                }
            } finally {
                try {
                    if(fis != null) {
                        fis.close();
                    }
                } catch(IOException ignored) {
                }
            }
        }
        //清除不存在的图片文件
        for(File file: imageFileList) {
            file.delete();
        }
    }

    /**
     * Invalidates an entry in the cache.
     * @param key Cache key
     * @param fullExpire True to fully expire the entry, false to soft expire
     */
    @Override
    public synchronized void invalidate(String key, boolean fullExpire) {
        Entry entry = get( key);
        if(entry != null) {
            entry.softTtl = 0;
            if(fullExpire) {
                entry.ttl = 0;
            }
            put( key, entry);
        }

    }

    /**
     * Puts the entry with the specified key into the cache.
     */
    @Override
    public synchronized void put(String key, Entry entry) {
        if(entry == null || entry.data == null || entry.data.length < 1) {
            return;
        }
        try {
            String path = new String( entry.data, DEFAULT_CHARSET);
            File imageFile = new File( path);
            pruneIfNeeded( imageFile.length());
            File file = getFileForKey( key);
            try {
                FileOutputStream fos = new FileOutputStream( file);
                CacheHeader e = new CacheHeader( key, entry);
                e.writeHeader( fos);
                // fos.write( entry.data);
                fos.close();
                putEntry( key, e);
                return;
            } catch(IOException e) {
                boolean deleted = file.delete() && imageFile.delete();
                if(!deleted) {
                    VolleyLog.d( "Could not clean up file %s", file.getAbsolutePath());
                }
            }

        } catch(UnsupportedEncodingException e1) {
            e1.printStackTrace();
        }

    }

    /**
     * Removes the specified key from the cache if it exists.
     */
    @Override
    public synchronized void remove(String key) {

        boolean deleted = getFileForKey( key).delete() && getImageFileForKey( key).delete();
        removeEntry( key);
        if(!deleted) {
            VolleyLog.d( "Could not delete cache entry for key=%s, filename=%s, imagefile=%s", key, getFilenameForKey( key),
                getImageFileForKey( key));
        }
    }

    /**
     * Creates a pseudo-unique filename for the specified cache key.
     * @param key The key to generate a file name for.
     * @return A pseudo-unique filename.
     */
    protected String getFilenameForKey(String key) {
        int firstHalfLength = key.length() / 2;
        String localFilename = String.valueOf( key.substring( 0, firstHalfLength).hashCode());
        localFilename += String.valueOf( key.substring( firstHalfLength).hashCode());
        return localFilename;
    }

    /**
     * Returns a file object for the given cache key.
     */
    public File getFileForKey(String key) {
        return new File( mRootDirectory, getFilenameForKey( key));
    }

    /**
     * Prunes the cache to fit the amount of bytes specified.
     * @param neededSpace The amount of bytes we are trying to fit into the cache.
     */
    protected void pruneIfNeeded(long neededSpace) {
        if((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }
        if(VolleyLog.DEBUG) {
            VolleyLog.v( "Pruning old cache entries.");
        }

        long before = mTotalSize;
        int prunedFiles = 0;
        long startTime = SystemClock.elapsedRealtime();

        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        while(iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey( e.key).delete();
            if(deleted) {
                mTotalSize -= e.size;
            }
            else {
                VolleyLog.d( "Could not delete cache entry for key=%s, filename=%s", e.key, getFilenameForKey( e.key));
            }
            iterator.remove();
            prunedFiles++;

            if((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        if(VolleyLog.DEBUG) {
            VolleyLog.v( "pruned %d files, %d bytes, %d ms", prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime()
                - startTime);
        }
    }

    /**
     * Puts the entry with the specified key into the cache.
     * @param key The key to identify the entry by.
     * @param entry The entry to cache.
     */
    protected void putEntry(String key, CacheHeader entry) {
        if(!mEntries.containsKey( key)) {
            mTotalSize += entry.size;
        }
        else {
            CacheHeader oldEntry = mEntries.get( key);
            mTotalSize += (entry.size - oldEntry.size);
        }
        mEntries.put( key, entry);
    }

    /**
     * Removes the entry identified by 'key' from the cache.
     */
    protected void removeEntry(String key) {
        CacheHeader entry = mEntries.get( key);
        if(entry != null) {
            mTotalSize -= entry.size;
            mEntries.remove( key);
        }
    }

    /**
     * Handles holding onto the cache headers for an entry.
     */
    // Visible for testing.
    static class CacheHeader {

        /**
         * The size of the data identified by this CacheHeader. (This is not serialized to disk.
         */
        public long size;

        /** The key that identifies the cache entry. */
        public String key;

        /** ETag for cache coherence. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        public byte[] data;

        /** Headers from the response resulting in this cache entry. */
        public Map<String, String> responseHeaders;

        private CacheHeader() {
        }

        /**
         * Instantiates a new CacheHeader object
         * @param key The key that identifies the cache entry
         * @param entry The cache entry.
         */
        public CacheHeader(String key, Entry entry) {
            this.key = key;
            this.size = entry.data.length;
            this.etag = entry.etag;
            this.serverDate = entry.serverDate;
            this.ttl = entry.ttl;
            this.softTtl = entry.softTtl;
            this.responseHeaders = entry.responseHeaders;
            this.data = entry.data;
        }

        /**
         * Reads the header off of an InputStream and returns a CacheHeader object.
         * @param is The InputStream to read from.
         * @throws IOException
         */
        public static CacheHeader readHeader(InputStream is) throws IOException {
            CacheHeader entry = new CacheHeader();
            int magic = readInt( is);
            if(magic != CACHE_MAGIC) {
                // don't bother deleting, it'll get pruned eventually
                throw new IOException();
            }
            entry.key = readString( is);
            entry.etag = readString( is);
            if(entry.etag.equals( "")) {
                entry.etag = null;
            }
            entry.serverDate = readLong( is);
            entry.ttl = readLong( is);
            entry.softTtl = readLong( is);
            entry.responseHeaders = readStringStringMap( is);
            String path = readString( is);
            if(!TextUtils.isEmpty( path)) {
                entry.data = path.getBytes();
            }
            return entry;
        }

        /**
         * Creates a cache entry for the specified data.
         */
        public Entry toCacheEntry() {
            Entry e = new Entry();
            e.data = data;
            e.etag = etag;
            e.serverDate = serverDate;
            e.ttl = ttl;
            e.softTtl = softTtl;
            e.responseHeaders = responseHeaders;
            return e;
        }

        /**
         * Writes the contents of this CacheHeader to the specified OutputStream.
         */
        public boolean writeHeader(OutputStream os) {
            try {
                writeInt( os, CACHE_MAGIC);
                writeString( os, key);
                writeString( os, etag == null ? "" : etag);
                writeLong( os, serverDate);
                writeLong( os, ttl);
                writeLong( os, softTtl);
                writeStringStringMap( responseHeaders, os);
                String path = (data == null || data.length < 1) ? "" : new String( data, DEFAULT_CHARSET);
                writeString( os, path);
                os.flush();
                return true;
            } catch(IOException e) {
                VolleyLog.d( "%s", e.toString());
                return false;
            }
        }
    }

    private void deleteFile(File file) {
        if(file == null || !file.exists())
            return;
        if(file.isFile()) {
            final File to = new File( file.getAbsolutePath() + System.currentTimeMillis());
            file.renameTo( to);
            to.delete();
        }
        else {
            File[] files = file.listFiles();
            if(files != null && files.length > 0) {
                for(File innerFile: files) {
                    deleteFile( innerFile);
                }
            }
            final File to = new File( file.getAbsolutePath() + System.currentTimeMillis());
            file.renameTo( to);
            to.delete();
        }
    }

    /**
     * Returns a image file object for the given cache key.
     */
    public File getImageFileForKey(String key) {
        return new File( mImageDir, getFilenameForKey( key));
    }

    public File getImageFileForKey(File file) {
        return new File( mImageDir, file.getName());
    }
}
